package roborally.model;
import exception.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;


import be.kuleuven.cs.som.annotate.*;
import be.kuleuven.cs.som.taglet.*;

/**
 * A board where the user can put elements such as robots, walls or batteries.
 * Elements can interact with each other on the board. They can also be removed.
 * Methods exist to clone, merge and terminate boards.
 * 
 * @invar ...
 * 		  | canHaveAsHeight(getHeight()) && canHaveAsWidth(getWidth())
 * 
 * @author Mathieu REYMOND
 * @author Joren VERSPEURT
 * 
 * @version 2.0
 *
 */

public class Board extends Terminatable {

	/**
	 * Creates a board with a given width and height.
	 * 
	 * @param width The width of the board
	 * @param height The height of the board
	 * @throws IllegalArgumentException
	 * 		   | !canHaveAsHeight(height) || !canHaveAsWidth(width)
	 * 
	 * @post ...
	 * 		 | height == new.getHeight() && width == new.getWidth()
	 */
	public Board(long width, long height) throws IllegalArgumentException {
		
		if(!canHaveAsHeight(height) || !canHaveAsWidth(width)) throw new IllegalArgumentException("width or height not valid");
		this.height = height;
		this.width = width;
		elementsAtPosition = new HashMap<Position, List<Element>>();
	}
	
	/**
	 * Checks if the given height is a valid height.
	 * 
	 * @param height The given height
	 * @return ...
	 * 		   | height > 0 && height <= Long.MAX_VALUE
	 */
	@Basic @Raw
	public boolean canHaveAsHeight(long height) {
		
		return height >= 0 && height <= Long.MAX_VALUE;
	}
	
	/**
	 * Checks if the given width is a valid width.
	 * 
	 * @param width The given width
	 * @return ...
	 * 		   | width > 0 && width <= Long.MAX_VALUE
	 */
	@Basic @Raw
	public boolean canHaveAsWidth(long width) {
		
		return width >= 0 && width <= Long.MAX_VALUE;
	}
	
	/**
	 * Returns this board's height.
	 */
	@Basic
	public long getHeight() {
		
		return height;
	}

	/**
	 * The Height of the board
	 */
	private final long height;

	/**
	 * Return this board's width.
	 */
	@Basic
	public long getWidth() {
		
		return width;
	}

	/**
	 * The width of the board
	 */
	private final long width;

	/**
	 * Put an element on this board. Please always use an element's setBoardPosition method to put an element on a board.
	 * 
	 * @param position The position to put the element on.
	 * @param element The element to put on the board.
	 * @throws NullPointerException
	 * 		  | position == null || element == null
	 * @throws IllegalPositionException
	 * 		  | !isValidPosition(position)
	 * @throws IllegalArgumentException
	 * 		  | element.getBoard() != null
	 * 		  | for any el in this.getElements(position)
	 * 		  | 	!element.canSharePositionWith(el)
	 * @throws IsTerminatedException
	 * 		  | element.isTerminated() || this.isTerminated()
	 * 
	 * @post ...
	 * 		| (new this).getElements(position).contains(element)
	 */
	public void putElement(Position position, Element element) throws NullPointerException, 
																		IllegalPositionException, 
																		IllegalArgumentException, 
																		IsTerminatedException { //amortized constant time ?
		
		if(position == null || element == null) throw new NullPointerException("position or element null");
		if(element.isTerminated() || this.isTerminated()) throw new IsTerminatedException("Element or this board is terminated");
		if(!isValidPosition(position) ) throw new IllegalPositionException("Not a valid position for this board");
		if(element.getBoard() != null) throw new IllegalArgumentException("This element is already on a board");
		boolean canShare = true;
		for(Element el : this.getElements(position)) {
			if(!element.canSharePositionWith(el)) {
				canShare = false;
			}
		}
		if(!canShare) throw new IllegalArgumentException("Cannot share this element with other elements on this position");
		
		List<Element> els = (List<Element>) getElements(position);
		els.add(element);
		elementsAtPosition.put(position, els);
		
	}

	/**
	 * Gives a set of all elements on this board of the given subclass of Element.
	 * 
	 * @param c The class of the elements to be returned
	 */
	@Basic
	public <T extends Element> java.util.Set<T> getElements(java.lang.Class<T> c) {
		
		Set<T> result = new HashSet<T>();
		for(List<Element> elements : elementsAtPosition.values()) {
			for(Element element : elements) {
				if(c.isAssignableFrom(element.getClass())) result.add((T) element);
			}
		}
		
		return result;
	}
	
	/**
	 * Returns all elements at a given position on the board.
	 * 
	 * @param position The position of the elements to be returned.
	 * @return ...
	 * 		   | let
	 * 		   | 	allElements = getElements(Element.class)
	 * 		   | in
	 * 		   | 	for each element in allElements
	 * 		   | 		if element.getPosition().equals(position)
	 * 		   | 		then result.contains(element)
	 * 		   | 		else !result.contains(element)
	 * @throws IllegalPositionException
	 * 		   | !isValidPosition(position)
	 * @throws NullPointerException
	 * 		   | position == null
	 */
	public java.lang.Iterable<Element> getElements(Position position) throws IllegalPositionException, NullPointerException{
		
		if(position == null) throw new NullPointerException("Position is null");
		if(!isValidPosition(position)) throw new IllegalPositionException("Not a valid position for this board");
		
		List<Element> result = new ArrayList<Element>();
		if(elementsAtPosition.get(position) != null) result = elementsAtPosition.get(position);
		
		return result;
	}
	
	/**
	 * Returns an iterator over this board that only returns Elements matching the condition that the result of invoking Method method on the Element
	 * must be less, less or equal, ... (in accordance with operator) than number.
	 * Naturally the methods passed to this method must have a Number or subtype of Number as return type.
	 * @param method The method to invoke on each element in the board.
	 * @param operator An instantiation of the ComparisonOperator Enum, representing a comparison operator (<,<=,==,!=,>= or >).
	 * @param number The right-hand-side of the comparison.
	 * @throws IllegalArgumentException 
	 * 		   | !Number.class.isAssignableFrom(method.getReturnType())
	 * @return ...
	 * 		   | for each element in result
	 * 		   | 	operator.check(method.invoke(currentElement), number)
	 */
	public Iterator<Element> getElementsSatisfying(final Method method, final ComparisonOperator operator, final Number number) throws IllegalArgumentException{
		if(!Number.class.isAssignableFrom(method.getReturnType())) throw new IllegalArgumentException("The return type of this method is not a number");
		return new Iterator<Element>() {
			
			/**
			 * Checks if there is an Element left to be returned by the iterator that satisfies the condition.
			 */
			@Override @Basic
			public boolean hasNext() {
				boolean result = false;
				if(currentElement != null) {
					return true;
				}
				while(currentIterator.hasNext()){
					try {
						
						currentElement = currentIterator.next();
						
						Number methodResult = (Number) method.invoke(currentElement);
						if(operator.check(methodResult, number)){
							result = true;
							break;
						}
						
					} catch (Exception e) {
						e.printStackTrace();
					}
					
				}
				
				return result;
			}
			
			/**
			 * Returns the next element in the iteration.
			 */
			@Override @Basic
			public Element next() {
				Element result;
				if(!hasNext()) throw new NoSuchElementException();
				else result = currentElement;
				
				currentElement = null;
				
				return result;
			}
			
			/**
			 * Removing elements from this iterator is not supported.
			 * @throws UnsupportedOperationException
			 * 		   | true
			 */
			@Override
			public void remove() throws UnsupportedOperationException{
				throw new UnsupportedOperationException();
				
			}
			
			/**
			 * The iterator's current element
			 */
			private Element currentElement;
			
			/**
			 * The current Iterator
			 */
			private Iterator<Element> currentIterator = getElements(Element.class).iterator();
			
		};
	}
	
	/**
	 * Returns an iterator over this board that only returns Elements matching the condition that invoking the Method method on that Element
	 * returns bool.
	 * Naturally the methods passed to this method must have boolean as return type.
	 * @param method The method to invoke on each element in the board.
	 * @param bool The boolean the result of invoking the method must be.
	 * @throws IllegalArgumentException
	 * 		   | !Boolean.class.isAssignableFrom(method.getReturnType())
	 * @return ...
	 * 		   | for each element in result
	 * 		   | 	method.invoke(currentElement) == bool
	 */
	public Iterator<Element> getElementsSatisfying(final Method method, final boolean bool) throws IllegalArgumentException{
		if(!Boolean.class.isAssignableFrom(method.getReturnType())) throw new IllegalArgumentException("The return type of this method is not a boolean");
		return new Iterator<Element>() {
			
			/**
			 * Checks if there is an Element left to be returned by the iterator that satisfies the condition.
			 */
			@Override @Basic
			public boolean hasNext() {
				boolean result = false;
				if(currentElement != null) {
					return true;
				}
				while(currentIterator.hasNext()){
					try {
						
						currentElement = currentIterator.next();
						
						boolean methodResult = (Boolean) method.invoke(currentElement);
						if(methodResult == bool){
							result = true;
							break;
						}
						
					} catch (Exception e) {
						e.printStackTrace();
					}
					
				}
				
				return result;
			}

			/**
			 * Returns the next element in the iteration.
			 */
			@Override @Basic
			public Element next() {
				Element result;
				if(!hasNext()) throw new NoSuchElementException();
				else result = currentElement;
				
				currentElement = null;
				
				return result;
			}

			/**
			 * Removing elements from this iterator is not supported.
			 * @throws UnsupportedOperationException
			 * 		   | true
			 */
			@Override
			public void remove() throws UnsupportedOperationException{
				throw new UnsupportedOperationException();
				
			}
			
			/**
			 * The iterator's current element
			 */
			private Element currentElement;
			
			/**
			 * The current Iterator
			 */
			private Iterator<Element> currentIterator = getElements(Element.class).iterator();
			
		};
	}
	
	
	/**
	 * A map with all elements linked to their position on the board
	 */
	private HashMap<Position, List<Element>> elementsAtPosition;

	/**
	 * Remove an element from the board.
	 * 
	 * @param element The element to remove
	 * @throws NullPointerException
	 * 		   | element == null
	 * @throws IsTerminatedException
	 * 		   | element.isTerminated()
	 * @throws IllegalArgumentException
	 * 		   | !element.getBoard().equals(this)
	 * 
	 * @post ...
	 * 		 | !(new this).getElements(Element.class).contains(element)
	 * @post ...
	 * 		 | (new element).getBoard() == null && (new element).getPosition() == null
	 */
	public void removeElement(Element element) throws NullPointerException, IsTerminatedException, IllegalArgumentException {
		
		if(element == null) throw new NullPointerException("Element is null");
		if(element.isTerminated()) throw new IsTerminatedException("Impossible to remove a terminated element : " + element);
		if(!element.getBoard().equals(this))
			throw new IllegalArgumentException("This element is on another board, impossible to remove it from this one");
		
		List<Element> newList = elementsAtPosition.get(element.getPosition());
		newList.remove(element);
		elementsAtPosition.put(element.getPosition(), newList);
		element.setBoardPosition(null, null);
	}

	/**
	 * Checks if Board other is compatible with this board, 
	 * meaning that their termination states must be equal, as well as their width and height.
	 * 
	 * @param other The other board.
	 * @return ...
	 * 		   | !(this.isTerminated() || other.isTerminated()) && (other.getWidth() == getWidth() && other.getHeight() == getHeight())
	 */
	public boolean isCompatibleWith(Board other) {
		return !(this.isTerminated() || other.isTerminated()) && (other.getWidth() == getWidth() && other.getHeight() == getHeight());
	}
	
	/**
	 * Check if the given position is a valid position for this board,
	 * meaning that it must be an effective position and the x and y must fit within the boundaries of the board.
	 * @param position The position to be checked.
	 * @return ...
	 * 		   | (position == null || position.getX() > getWidth() || position.getY() > getWidth())
	 */
	@Raw
	public boolean  isValidPosition(Position position) {
		
		boolean result = true;
		if(position == null || position.getX() > getWidth() || position.getY() > getWidth()) result = false;
		
		return result;
	}
	
	/**
	 * Merge Board other with this board. All elements on the other board are placed at their respective position on this board,
	 * if possible. If this isn't possible because the position doesn't exist the element is removed. If it isn't possible because
	 * the position is already taken, placing the element on a neighbouring position is attempted. If even this is not possible the
	 * element is terminated.
	 * 
	 * @param other The other board to merge with.
	 * @throws NullPointerException
	 * 		  | other == null
	 * @throws IsTerminatedException
	 * 		  | this.isTerminated() || other.isTerminated()
	 * 
	 * @post ...
	 * 		 | (new other).isTerminated()
	 * @post ...
	 * 		 | for each element in other.getElements(Element.class)
	 * 		 | 		if !this.isValidPosition(element.getPosition())
	 * 		 | 		then (new element).isTerminated()
	 *  	 | 		else
	 * 		 | 			for each e in this.getElements(element.getPosition())
	 * 		 |				if !element.canSharePositionWith(e)
	 * 		 | 				then 
	 * 		 | 					let neighbouringPositions, possiblePositions in
	 * 		 | 					for each orientation in Orientation.values()
	 * 		 | 						neighbouringPositions.contains(position.getNeighbour(orientation))
	 * 		 | 					for each position in neighbouringPositions
	 * 		 | 						for each el in this.getElements(position)
	 * 		 | 							if !element.canSharePositionWith(e)
	 * 		 | 							then !possiblePositions.contains(position)
	 * 		 | 					possiblePositions.isEmpty() || (possiblePositions.contains((new element).getPosition()) && neighbouringPositions.contains((new element).getPosition())
	 * 		 | 					if possiblePositions.isEmpty()
	 * 		 | 					then (new element).isTerminated()
	 * 		 | 					else (new this).getElements(Element.class).contains(element)
	 * 		 | 				else
	 * 		 | 					(new this).getElements(Element.class).contains(element)
	 */
	public void merge(Board other) throws NullPointerException, IsTerminatedException{
		
		if(other == null) throw new NullPointerException("Board is null");
		if(this.isTerminated()) throw new IsTerminatedException("This board is terminated");
		if(other.isTerminated()) throw new IsTerminatedException("The other board is terminated");
		
		for(Element element: other.getElements(Element.class)) {
			if(!isValidPosition(element.getPosition())) ;//element.terminate();
			else {
				
				Position elementPos =new Position(element.getPosition().getX(), element.getPosition().getY());
				other.removeElement(element);
				try {
					element.setBoardPosition(this, elementPos);
				}
				catch(Exception e) {
					boolean couldPlace = false;
					for(Orientation ori : Orientation.values()){
						try{
							if(!couldPlace) element.setBoardPosition(this,elementPos.getNeighbour(ori));
							couldPlace = true;
						}
						catch(Exception x){

						}
					}
					if(!couldPlace) ;//element.terminate();
				}
			}
		}
		
		other.terminate();
	}
	
	/**
	 *Clone the current board.
	 *
	 *@return ...
	 *		  | result.getWidth() == this.getWidth && result.getHeight() == this.getHeight()
	 *@return ...
	 *		  | this.getElements(Element.class).equals(result.getElements(Element.class))
	 */
	@Override
	public Board clone() {
		
		Board newBoard = new Board(getWidth(), getHeight());
		for(Element element : getElements(Element.class)) {
			Element clone = element.clone(); //we clone the element
			Position clonePos =new Position(element.getPosition().getX(), element.getPosition().getY()); //we save the element's position
			clone.setBoardPosition(newBoard, clonePos);
		}
		if(this.isTerminated) {
			newBoard.terminate();
		}
		return newBoard;
	}
	
	/**
	 * Terminate this board and all the elements that are on it.
	 * 
	 * @post ...
	 * 		 | for each element in this.getElements(Element.class)
	 * 		 |		(new element).isTerminated()
	 * 		 |		!(new this).getElements(Element.class).contains(element) 
	 */
	@Override
	public void terminate() {
		
		for(List<Element> elements : elementsAtPosition.values()) {
			for(int i = elements.size() -1; i >= 0; i--) {
				Element toBeTerminated = elements.get(i);
				removeElement(toBeTerminated);
				toBeTerminated.terminate();
				
			}
		}
		
		super.terminate(); //terminate the board.
	}
	
	/**
	 * Return a String with information concerning this board
	 * 
	 * @return ...
	 * 		   | !result.isEmpty()
	 */
	@Override
	public String toString() {
		if(this.isTerminated()) return "A terminated board";
		String result = "Board(" + getWidth() + ", " + getHeight() + ") + [";
		for(Element element : getElements(Element.class)) {
			result += "\n" + element.toString();
		}
		
		return result;
	}
}
